C|从数组指针的角度深刻理解数组名是一个指向数组首元素的地址

数组用于储存相同类型的数据。C把数组看作是派生类型,因为数组是建立在其他类型的基础上。也就是说,无法简单地声明一个数组。在声明数组时必须说明其元素的类型,如int 类型的数组、float 类型的数组,或其他类型的数组。所谓的其他类型也可以是数组类型,这种情况下,创建的是数组的数组(或称为二维数组)。

1 数组名与指向该数组首元素的指针等价

C把数组名解释为该数组首元素的地址。换言之,数组名与指向该数组首元素的指针等价。概括地说,数组和指针的关系十分密切。如果ar 是一个数组,那么表达式ar[i]和* (ar+i) 等价。

通常编写一个函数来处理数组,这样在特定的函数中解决特定的问题,有助于实现程序的模块化。在把数组名作为实际参数时,传递给函数的不是整个数组,而是数组的地址(因此,函数对应的形式参数是指针)。为了处理数组,函数必须知道从何处开始读取数据和要处理多少个数组元素。数组地址提供了“ 地址", "元素个数” 可以内置在函数中或作为单独的参数传递。第2种方法更普遍,因为这样做可以让同一个函数处理不同大小的数组。

数组和指针的关系密切,同一个操作可以用数组表示法或指针表示法。它们之间的关系允许你在处理数组的函数中使用数组表示法,即使函数的形式参数是一个指针,而不是数组。

数组名是指向数组首元素的地址,因此可以赋值给一个同类型、同维度的指针变量:

int a1[] = {1,2,3}; // a1指向数组首元素的地址,其元素是一个int,所于al可以赋值给一个int*指针

int* p1 = a1; // []的*在一定程序上的等价性

int a2[][4] = {1,2,3,4,5,6,7,8,9,10,11,12}; //a2指向数组首元素的地址,

// 首元素是一个一(2-1)维的数组,所以a2等价于&a2[0],a2还有行数3的信息

int (*p2)[4] = a2;

int a3[][3][4] = {1,2,3,4,5,6,7,8,9,10,11,12, // a3指向数组首元素的地址

13,14,15,16,17,18,19,20,21,22,23,24}; // 首元素是一个二(3-1)维的数组

int (*p3)[3][4] = a3;

如果a是一个n维数组,则数组的元素就是一个n-1维的数组,a指向数组首元素的地址,就是指向n-1维数组的地址。

二维数组即是数组的数组。例如,下面声明了一个二维数组:

int zippo[4][2]; ; // 内含int数组的数组

该数组名为zippo, 有4个元素( 一维数组),每个元素都是一个内含2个int 类型值的数组。

第1个一维数组是zippo[0] , 第2个一维数组是zippo [1],以此类推,每个元素都是内含2个int类型值的数组。使用第2个下标可以访问这些一维数组中的特定元素。例如, zippo[1][1] 是zippo[1]的第2个元素,而zippo[3]是sales 的第4个元素。

数组名 zippo 是该数组首元素的地址。 在本例中, zippo 的首元素是一个内含两个 int 值的数组,所以 zippo 是这个内含两个 int 值的数组的地址。

因为 zippo 是数组首元素的地址,所以 zippo 的值和 &zippo[0]的值相同。 而 zippo[0] 本身是一个内含两个整数的数组,所以 zippo[0]的值和它首元素( 一个整数)的地址(即 &zippo[O] [0] 的值)相同。 简而言之, zippo[0]是一个占用一个 int 大小对象的地址,而 zippo 是一个占用两个 int 大小对象的地址。由于这个整数和内含两个整数的数组都开始于同一个地址,所以 zippo 和 zippo[0]的值相同。

给指针或地址加1,其值会增加对应类型大小的数值。在这方面,zippo 和zippo[0]不同,因为zippo 指向的对象占用了两个 int 大小,而 zippo[0]指向的对象只占用一个 int 大小。因此,zippo +1 和zippo[0]+1 的值不同。前者是移动2个int、8个字节(32位系统),后者只移动1个int、4个字节。

解引用一个指针(在指针前使用*运算符)或在数组名后使用带下标的[]运算符,得到引用对象代表的值。因为zippo[0]是该数组首元素(zippo[0][0])的地址,所以*(zippo[0])表示储存在zippo[0][0]上的值(即一个 int 类型的值)。与此类似,*zippo 代表该数组首元素(zippo[0])的值,但是zippo[0]本身是一个int类型值的地址。该值的地址是&zippo[0][0],所以*zippo就是&zippo[0][0]。对两个表达式应用解引用运算符表明,**zippo与*&zippo[0][0]等价,这相当于zippo[0][0],即一个int类型的值。简而言之,zippo是地址的地址,必须解引用两次才能获得原始值。地址的地址或指针的指针是就是双重间接(double indirection)的例子。

zippo = 0x0064fd38,zippo +1 = 0x0064fd40

zippo[0] = 0x0064fd38,zippo[0]+1 = 0x0064fd3c

*zippo = 0x0064fd38,*zippo +1 = 0x0064fd3c

zippo[0][0] = 2

*zippo[0] = 2

**Zippo = 2

zippo[2][1] = 3

*(*(zippo+2)+1)=3

其他系统显示的地址值和地址形式可能不同,但是地址之间的关系与以上输出相同。该输出显示了二维数组zippo 的地址和一维数组zippo[0]的地址相同。它们的地址都是各自数组首元素的地址,因而与&zippo[0][0] 的值也相同。

尽管如此,它们也有差别。在我们的系统中,int 是4字节。前面讨论过,zippo[0]指向一个4字节的数据对象。zippo[0]加1,其值加4 (十六进制中,38+4 得3c)。数组名zippo 是一个内含2 个int 类型值的数组的地址,所以zippo 指向一个8字节的数据对象。因此,zippo 加1,它所指向的地址加8字节(十六进制中,38+8 得40)。

如何声明一个指针变量pz 指向一个二维数组(如,zippo)?在编写处理类似zippo 这样的二维数组时会用到这样的指针。把指针声明为指向int 的类型还不够。因为指向int 只能与zippo[0]的类型匹配,说明该指针指向一个int 类型的值。但是zippo 是它首元素的地址,该元素是一个内含两个int 类型值的一维数组。因此,pz 必须指向一个内含两个int 类型值的数组,而不是指向一个int 类型值,其声明如下:

int (*pz)[2]; //pz 指向一个内含两个int 类型值的数组

以上代码把pz 声明为指向一个数组的指针,该数组内含两个int 类型值。为什么要在声明中使用圆括号?因为 [] 的优先级高于*。考虑下面的声明:

int *pax [2]; // pax 是一个内含两个指针元素的数组,每个元素都指向int 的指针

由于 [] 优先级高,先与pax 结合,所以pax 成为一个内含两个元素的数组。然后*表示pax 数组内含两个指针。最后,int 表示pax 数组中的指针都指向int 类型的值。因此,这行代码声明了两个指向int的指针。

而前面有圆括号的版本,*先与pz 结合,因此声明的是一个指向数组(内含两个int 类型的值)的指针。

int (*pz)[2] = zippo;

pz = 0x0064fd38, pz + 1 = 0x0064fd40

pz[0] = 0x0064fd38, pz[0] + 1 = 0x0064fd3c

*pz = 0x0064fd38, *pz + 1 = 0x0064 fd3c

pz [0][0] = 2

*pz[0] = 2

**pz = 2

pz[2][1] = 3

*{*(pz+2) + 1) = 3

这里反映pz与zippo具有相同的运算规则。指针加上一个整数或递增指针,指针的值以所指向对象的大小为单位改变。也就是说,如果pd 指向一个数组的4字节int类型值,那么pd 加1意味着其值加4,以便它指向该数组的下一个元素。

zippo[m][n]==*(*(zippo +m)+n)

pz[m][n]==*(*(pz +m)+n)

2 数组作为函数参数时实参与形参的结合

对于C语言而言,不能把整个数组作为参数传递给函数,但是可以传递数组的地址。然后函数可以使用传入的地址操控原始数组。如果函数没有修改原始数组的意图,应在声明函数的形式参数时使用关键字const。在被调函数中可以使用数组表示法或指针表示法,无论用哪种表示法,实际上使用的都是指针变量。

可以这样声明函数的形参:

void somefunction (int (*pt)[4]);

另外,如果当且仅当pt是一个函数的形式参数时,可以这样声明:

void somefunction(int pt[][4]);

int sum2(int ar[][], int rows);// 错误的声明

编译器会把数组表示法转换成指针表示法。例如,编译器会把ar[i] 转换成ar+i。编译器对ar+i 求值,要知道ar 所指向的对象大小。下面的声明:

int sum2 (int ar[][4] , int rows); //有效声明

表示ar 指向一个内含4 个int 类型值的数组(在32位系统中, ar 指向的对象占16 字节),所以ar+i的意思是“ 该地址加上16 字节”。如果第2 对方括号是空的,编译器就不知道该怎样处理。

也可以在第1对方括号中写上大小,但是编译器会忽略该值。

字符串有一些特殊的规则,这是由于其末尾的空字符所致。有了这个空字符,不用传递数组的大小,函数通过检测字符串的末尾也知道在何处停止。

所以总结一下,声明时,首维长度可以省略,因编译器自动推断长度。因为数组名是指向数组首元素的地址,是一个有常量性质的指针,所以这里的[]相当于*,但与指针不同的是,数组名还有首维的长度信息。数组做参数时,数组退化为指针,参数是传址,对于n维数组,首维长度可以省略(其它维不能),相当于是指针的信息。

3 变长数组

对于传统的C数组,必须用常量表达式指明数组的大小,所以数组大小在编译时就已确定。C99/C11新增了变长数组,可以用变量表示数组大小。这意味着变长数组的大小延迟到程序运行时才确定。

C99新增的变长数组(variable-length array, VLA)允许使用变量表示数组的维度。如下所示:

int quarters = 4;

int regions = 5;

double sales [regions] [quarters]; // 一个变长数组(VLA)

变长数组有一些限制。变长数组必须是自动存储类别,这意味着无论在函数中声明还是作为函数形参声明,都不能使用static 或extern 存储类别说明符。而且,不能在声明中初始化它们。最终,Cl1把变长数组作为一个可选特性,而不是必须强制实现的特性。

注意变长数组不能改变大小变长数组中的“变” 不是指可以修改已创建数组的大小。一旦创建了变长数组,它的大小则保持不变。这里的“ 变” 指的是:在创建数组时,可以使用变量指定数组的维度。

由千变长数组是C语言的新特性,目前完全支持这一特性的编译器不多。

C 语言传递多维数组的传统方法是把数组名(即数组的地址)传递给类型匹配的指针形参。声明这样的指针形参要指定所有的数组维度,除了第1个维度。传递的第1个维度通常作为第2个参数。例如,为了处理前面声明的sales 数组,函数原型和函数调用如下:

void display(double ar[][12], int rows);

. . .

display(sales, 5);

变长数组提供第2种语法,把数组维度作为参数传递。在这种情况下,对应函数原型和函数调用如下:

void display(int rows, int cols, double ar[rows] [cols]);

. . .

display(5, 12, sales);

-End-

本页共83段,5780个字符,12021 Byte(字节)